// 限流管理器

import { BaseManager } from '../core/BaseManager';
import { RedisClient } from '../core/RedisClient';
import { KeyGenerator } from '../utils/keyGenerator';

export interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetTime: Date;
  retryAfter: number | undefined;
}

export interface RateLimitConfig {
  windowSize: number; // 时间窗口大小（秒）
  limit: number; // 限制次数
  precision?: number; // 精度（秒）
}

export interface TokenBucketConfig {
  capacity: number; // 桶容量
  refillRate: number; // 补充速率（每秒）
  initialTokens?: number; // 初始令牌数
}

export class RateLimitManager extends BaseManager {
  constructor(client: RedisClient) {
    super(client, new KeyGenerator({ prefix: 'ratelimit' }));
  }

  /**
   * 固定窗口限流
   */
  async checkFixedWindow(key: string, limit: number, window: number): Promise<RateLimitResult> {
    this.validateRequired({ key });
    this.validateNumber(limit, 'limit', 1);
    this.validateNumber(window, 'window', 1);

    const now = Math.floor(Date.now() / 1000);
    const windowStart = Math.floor(now / window) * window;
    const windowKey = this.keyGenerator.rateLimit(key, windowStart.toString());

    const startTime = this.now();

    try {
      // 获取当前计数
      const current = await this.execute<number>('incr', windowKey);

      // 设置过期时间
      if (current === 1) {
        await this.setExpire(windowKey, window);
      }

      const allowed = current <= limit;
      const remaining = Math.max(0, limit - current);
      const resetTime = new Date((windowStart + window) * 1000);

      this.logMetric('rateLimit.fixedWindow', this.now() - startTime, allowed, {
        key,
        current,
        limit,
        window,
      });

      return {
        allowed,
        remaining,
        resetTime,
        retryAfter: allowed ? undefined : windowStart + window - now,
      };
    } catch (error) {
      this.logMetric('rateLimit.fixedWindow', this.now() - startTime, false, { key, error });
      throw this.formatError(error, 'Failed to check fixed window rate limit');
    }
  }

  /**
   * 滑动窗口限流
   */
  async checkSlidingWindow(
    key: string,
    limit: number,
    window: number,
    precision: number = 60
  ): Promise<RateLimitResult> {
    this.validateRequired({ key });
    this.validateNumber(limit, 'limit', 1);
    this.validateNumber(window, 'window', 1);
    this.validateNumber(precision, 'precision', 1);

    const now = Math.floor(Date.now() / 1000);
    const currentBucket = Math.floor(now / precision) * precision;
    const bucketKey = this.keyGenerator.rateLimit(key, 'sliding');

    const startTime = this.now();

    try {
      // 使用Lua脚本确保原子性
      const luaScript = `
        local key = KEYS[1]
        local now = tonumber(ARGV[1])
        local window = tonumber(ARGV[2])
        local limit = tonumber(ARGV[3])
        local precision = tonumber(ARGV[4])
        local bucket = tonumber(ARGV[5])
        
        -- 清理过期的桶
        local windowStart = now - window
        redis.call('zremrangebyscore', key, '-inf', windowStart)
        
        -- 获取当前窗口内的总计数
        local current = 0
        local buckets = redis.call('zrangebyscore', key, windowStart, now, 'WITHSCORES')
        for i = 1, #buckets, 2 do
          current = current + tonumber(buckets[i])
        end
        
        -- 检查是否超过限制
        if current >= limit then
          return {0, current, limit - current}
        end
        
        -- 增加当前桶的计数
        redis.call('zincrby', key, 1, bucket)
        redis.call('expire', key, window + precision)
        
        return {1, current + 1, limit - current - 1}
      `;

      const result = await this.execute<number[]>(
        'eval',
        luaScript,
        1,
        bucketKey,
        now,
        window,
        limit,
        precision,
        currentBucket
      );

      const [allowedNum, current, remaining] = result;
      const allowed = allowedNum === 1;
      const resetTime = new Date((now + window) * 1000);

      this.logMetric('rateLimit.slidingWindow', this.now() - startTime, allowed, {
        key,
        current,
        limit,
        window,
        precision,
      });

      return {
        allowed,
        remaining: Math.max(0, remaining ?? 0),
        resetTime,
        retryAfter: allowed ? undefined : precision,
      };
    } catch (error) {
      this.logMetric('rateLimit.slidingWindow', this.now() - startTime, false, { key, error });
      throw this.formatError(error, 'Failed to check sliding window rate limit');
    }
  }

  /**
   * 令牌桶限流
   */
  async checkTokenBucket(key: string, config: TokenBucketConfig): Promise<RateLimitResult> {
    this.validateRequired({ key, config });
    this.validateNumber(config.capacity, 'capacity', 1);
    this.validateNumber(config.refillRate, 'refillRate', 0);

    const bucketKey = this.keyGenerator.rateLimit(key, 'bucket');
    const now = Date.now() / 1000;

    const startTime = this.now();

    try {
      // 使用Lua脚本实现令牌桶算法
      const luaScript = `
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local refillRate = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        local initialTokens = tonumber(ARGV[4])
        
        -- 获取桶状态
        local bucket = redis.call('hmget', key, 'tokens', 'lastRefill')
        local tokens = tonumber(bucket[1]) or initialTokens
        local lastRefill = tonumber(bucket[2]) or now
        
        -- 计算需要补充的令牌
        local timePassed = now - lastRefill
        local tokensToAdd = math.floor(timePassed * refillRate)
        tokens = math.min(capacity, tokens + tokensToAdd)
        
        -- 检查是否有可用令牌
        if tokens < 1 then
          -- 更新桶状态
          redis.call('hmset', key, 'tokens', tokens, 'lastRefill', now)
          redis.call('expire', key, 3600)
          return {0, tokens, (1 - tokens) / refillRate}
        end
        
        -- 消费一个令牌
        tokens = tokens - 1
        
        -- 更新桶状态
        redis.call('hmset', key, 'tokens', tokens, 'lastRefill', now)
        redis.call('expire', key, 3600)
        
        return {1, tokens, 0}
      `;

      const result = await this.execute<number[]>(
        'eval',
        luaScript,
        1,
        bucketKey,
        config.capacity,
        config.refillRate,
        now,
        config.initialTokens ?? config.capacity
      );

      const [allowedNum, remaining, retryAfter] = result;
      const allowed = allowedNum === 1;

      this.logMetric('rateLimit.tokenBucket', this.now() - startTime, allowed, {
        key,
        remaining,
        capacity: config.capacity,
        refillRate: config.refillRate,
      });

      return {
        allowed,
        remaining: Math.floor(remaining ?? 0),
        resetTime: new Date(Date.now() + (config.capacity / config.refillRate) * 1000),
        retryAfter: allowed ? undefined : Math.ceil(retryAfter ?? 0),
      };
    } catch (error) {
      this.logMetric('rateLimit.tokenBucket', this.now() - startTime, false, { key, error });
      throw this.formatError(error, 'Failed to check token bucket rate limit');
    }
  }

  /**
   * 分布式限流
   */
  async checkDistributed(key: string, limit: number, window: number): Promise<RateLimitResult> {
    this.validateRequired({ key });
    this.validateNumber(limit, 'limit', 1);
    this.validateNumber(window, 'window', 1);

    // 使用滑动窗口实现分布式限流
    return this.checkSlidingWindow(key, limit, window);
  }

  /**
   * 重置限流计数器
   */
  async reset(key: string): Promise<void> {
    this.validateRequired({ key });

    const pattern = this.keyGenerator.rateLimit(key, '*');
    const keys = await this.getKeys(pattern);

    if (keys.length > 0) {
      await this.deleteKeys(...keys);
    }
  }

  /**
   * 获取限流状态
   */
  async getStatus(key: string, type: 'fixed' | 'sliding' | 'bucket' = 'fixed'): Promise<any> {
    this.validateRequired({ key });

    switch (type) {
      case 'fixed':
        return this.getFixedWindowStatus(key);
      case 'sliding':
        return this.getSlidingWindowStatus(key);
      case 'bucket':
        return this.getTokenBucketStatus(key);
      default:
        throw new Error(`Unsupported rate limit type: ${type}`);
    }
  }

  /**
   * 获取固定窗口状态
   */
  private async getFixedWindowStatus(key: string): Promise<any> {
    const now = Math.floor(Date.now() / 1000);
    const pattern = this.keyGenerator.rateLimit(key, '*');
    const keys = await this.getKeys(pattern);

    const status: any = {
      type: 'fixed',
      windows: [],
    };

    for (const windowKey of keys) {
      const count = await this.getValue<string>(windowKey);
      const windowStart = parseInt(windowKey.split(':').pop() ?? '0', 10);

      status.windows.push({
        start: new Date(windowStart * 1000),
        count: parseInt(count ?? '0', 10),
        active: windowStart <= now,
      });
    }

    return status;
  }

  /**
   * 获取滑动窗口状态
   */
  private async getSlidingWindowStatus(key: string): Promise<any> {
    const bucketKey = this.keyGenerator.rateLimit(key, 'sliding');
    const buckets = await this.execute<string[]>('zrange', bucketKey, 0, -1, 'WITHSCORES');

    const status: any = {
      type: 'sliding',
      buckets: [],
      totalCount: 0,
    };

    for (let i = 0; i < buckets.length; i += 2) {
      const countStr = buckets[i];
      const timestampStr = buckets[i + 1];

      if (countStr && timestampStr) {
        const count = parseInt(countStr, 10);
        const timestamp = parseInt(timestampStr, 10);

        status.buckets.push({
          timestamp: new Date(timestamp * 1000),
          count,
        });

        status.totalCount += count;
      }
    }

    return status;
  }

  /**
   * 获取令牌桶状态
   */
  private async getTokenBucketStatus(key: string): Promise<any> {
    const bucketKey = this.keyGenerator.rateLimit(key, 'bucket');
    const bucket = await this.execute<string[]>('hmget', bucketKey, 'tokens', 'lastRefill');

    return {
      type: 'bucket',
      tokens: parseFloat(bucket[0] ?? '0'),
      lastRefill: bucket[1] ? new Date(parseFloat(bucket[1]) * 1000) : null,
    };
  }

  /**
   * 批量检查限流
   */
  async checkBatch(
    keys: string[],
    limit: number,
    window: number,
    type: 'fixed' | 'sliding' = 'fixed'
  ): Promise<Record<string, RateLimitResult>> {
    this.validateRequired({ keys });

    const results: Record<string, RateLimitResult> = {};

    // 并行检查所有键
    const promises = keys.map(async key => {
      try {
        const result =
          type === 'fixed'
            ? await this.checkFixedWindow(key, limit, window)
            : await this.checkSlidingWindow(key, limit, window);
        return { key, result };
      } catch (error) {
        return {
          key,
          result: {
            allowed: false,
            remaining: 0,
            resetTime: new Date(),
            retryAfter: window,
          },
        };
      }
    });

    const batchResults = await Promise.all(promises);

    for (const { key, result } of batchResults) {
      results[key] = result;
    }

    return results;
  }
}
